/*=============================================================================
    jibits/events.js
    Copyright (C) 2008-2009, Donald W. Griffin
    All rights reserved.
    [MIT license :: see license.txt for details]
=============================================================================*/
$module("jibits.events");

$requires("zjs.ext");
$requires("zjs.browser");

$requires("jibits.dom");

$namespace(jibits, {

/**
This class manages various cleanup tasks as a group. When this object's destroy
method is called, all registered cleanup methods are called (and forgotten).
*/
Ticket : $class(null,
{
    ctor : function ()
    {
        this.cleaners = [];
    },

    /**
    Adds a function or destroyable object to this ticket. Functions are added to
    this cleaners list. Destroyable objects are added such that their destroy
    method will be called. Functions will be passed this ticket as an argument,
    but the destroy method will be called in the context of the destroyable
    object with no arguments.

    @param fn The function or destroyable object.
    */
    add : function (fn)
    {
        if (!(fn instanceof Function))
            fn = fn.destroy.bind(fn).seal(); // zjs.ext

        this.cleaners.push(fn);
        return this;
    },

    destroy : function ()
    {
        var keeper = this.cleaners, self = this;
        this.cleaners = [];

        $foreach(keeper, function (fn)
        {
            fn(self);
        });
    }
}),

/**
This class creates Ticket objects and (like a Ticket) responds to destroy. When
the destroy method is called on this object, all tickets it has created will be
destroyed (and forgotten).
*/
TicketManager : $class(null, function ()
{
    /**
    Called to cleanup a single ticket.
    */
    function cleanup (ticket)
    {
        if (!(ticket.id in this.tickets))
            return;

        delete this.tickets[ticket.id];
        -- this.ticketCount;
    }

    return (
    {
        ctor : function ()
        {
            this.cleanup = cleanup.bind(this); // zjs.ext
            this.ticketCount = 0;
            this.ticketId = 0;
            this.tickets = {};
        },

        destroy : function ()
        {
            var t, keeper = this.tickets;
            this.tickets = {};
            this.ticketCount = 0;

            for (var tid in keeper)
                if ((t = keeper[tid]))
                    t.destroy();
        },

        /**
        Creates a new ticket and tracks it for later cleanup. Optionally, an
        initial cleanup method can be supplied that will be added to the new
        Ticket.

        @param fn The optional initial cleanup method.
        @return The new Ticket object.
        */
        newTicket : function (fn)
        {
            var ticket = new jibits.Ticket();
            ticket.id = ++this.ticketId;
            ticket.add(this.cleanup);

            this.tickets[ticket.id] = ticket;
            ++this.ticketCount;
            if (fn)
                ticket.add(fn);

            return ticket;
        }
    });
}()),

/**
Subscribes for events of the given type from an element. This function only
masks the basic differences between browsers. For cross-browser use, use the
jibits.listener method to wrap the event handler. For automatic cleanup, use
EventManager instead. The counterpart to this method is unsubscribe.

@param el The element to which to subscribe.
@param event The event name (e.g., "mouseover").
@param handler The event handler method.
@return The given element.
*/
subscribe : function (el, event, handler, useCapture)
{
    el = $elem(el);
    if (el.addEventListener)
        el.addEventListener(event, handler, useCapture);
    else // if (IE)
        el.attachEvent("on" + event, handler); // no mapping for useCapture
    return el;
},

/**
Unsubscribes for events of the given type from an element. This function only
masks the basic differences between browsers. For automatic cleanup, use the
EventManager instead.

@param el The element to which to unsubscribe.
@param event The event name (e.g., "mouseover").
@param handler The event handler method.
@return The given element.
*/
unsubscribe : function (el, event, handler, useCapture)
{
    el = $elem(el);
    if (el.removeEventListener)
        el.removeEventListener(event, handler, useCapture);
    else // if (IE)
        el.detachEvent("on" + event, handler); // no mapping for useCapture
    return el;
},

/**
This class manages events (cleanup in particular) for a window or frame. These
instances are acquired by the static get method which accepts the window (or
frame) to which the object is associated. Each window/frame has need of only
one EventManager. This is managed by the jibits.eventMgr method. The EventManager
sets up an event listener for the unload event so that it can automatically
remove any event handlers it has registered. This is most helpful in IE which
can easily leak member of event handlers are not removed.
*/
EventManager : $class(null, function ()
{
    function cleanup (useCapture, ticket)
    {
        var el = $elem(ticket.target);
        jibits.unsubscribe(el, ticket.event, ticket.handler, useCapture);
    }

    return (
    {
        /**
        Initializes this object. This object is connected to the owner via an
        onunload event handler to ensure all event subscriptions get cleaned
        up (to avoid IE memory leaks).

        @param owner The window or frame associated with this object.
        */
        ctor : function (owner)
        {
            $super(arguments).call(this, owner);

            this.cleanup = cleanup.bind(this); // zjs.ext
            this.owner = owner || window;
            this.ticketMgr = new jibits.TicketManager();

            this.listen(this.owner, "unload", this.destroy.bind(this));
        },

        destroy : function ()
        {
            var meta = jibits.getMeta(this.owner, true);
            if (meta && meta.mgr === this)
                meta.mgr = null;

            this.ticketMgr.destroy();
        },

        /**
        Sets up the given handler to listen for specified events on an element.
        The event handler can be managed using the returned Ticket object. To
        unsubscribe, call the destroy method of the Ticket. This is not needed
        if the subscription lasts for as long as the page is loaded as this
        object will automatically disconnect all such handlers at onunload
        time.

        The given event handler is wrapped using jibits.listener, so see that
        method for details. A common use case would be:

            this.mouseMove = jibits.eventMgr().listen( ... );

        And later, when the event is no longer needed:

            this.mouseMove.destroy();
        */
        listen : function (el, event, handler, useCapture)
        {
            el = $elem(el);

            var fn = jibits.listener(handler, el.id);

            var ticket = this.newTicket(this.cleanup.head(useCapture));
            ticket.event = event;
            ticket.handler = fn;
            ticket.target = el.id || el;

            jibits.subscribe(el, event, fn, useCapture);

            return ticket;
        },

        newTicket : function (fn)
        {
            return this.ticketMgr.newTicket(fn);
        }
    });
}())

}); // namespace jibits

//-----------------------------------------------------------------------------
// Event normalization:

if (zjs.browser.ie)
    $namespace(jibits, function ()
    {
        return (
        {
            /**
            This class is used to provide the user-facing event instance on IE.
            The IE event object (window.event) is too far from W3C format to do
            much less. This provides the opportunity to provide a standard shape
            to the
            */
            EventWrapper : $class(null,
            {
                ctor : function (ev)
                {
                    zjs.copyProps(this, ev);
                    this._event = ev; // the raw event object

                    this.relatedTarget = (ev.type == "mouseover")
                                            ? ev.fromElement : ev.toElement;
                    this.target = ev.srcElement;

                    // see http://www.quirksmode.org/dom/w3c_cssom.html
                    // and http://www.quirksmode.org/dom/tests/mouseposition.html
                    var pos = jibits.getViewportScroll();
                    this.pageX = pos.x + ev.clientX;
                    this.pageY = pos.y + ev.clientY;
                },

                consume : function ()
                {
                    this.preventDefault();
                    this.stopPropagation();
                },

                preventDefault : function ()
                {
                    this._event.returnValue = false;

                    // Keys are harder to prevent (metakeys like CTRL/SHIFT may
                    // complain):
                    if (this._event.keyCode)
                        try { this._event.keyCode = 0; } catch (e) {}
                },

                stopPropagation : function ()
                {
                    this._event.cancelBubble = true;
                }
            }),

            /**
            This internal method is used by jibits.listener.
            */
            _wrapListener : function (handler, id)
            {
                return function (e)
                {
                    var ev = new jibits.EventWrapper(e || window.event);

                    if (id)
                    {
                        ev.target = $elem(id) || ev.target;
                        /*var fix = ev.target;

                        try
                        {
                            while (fix && fix.id != id)
                                fix = fix.parentNode;
                        }
                        catch (e) { }

                        if (fix)
                            ev.target = fix;*/
                    }

                    return handler(ev);
                };
            }
        });
    }());
else
    $namespace(jibits, function ()
    {
        var methods =
        {
            consume : function ()
            {
                this.preventDefault();
                this.stopPropagation();
            },

            preventDefault : function ()
            {
                this._event.preventDefault();
            },

            stopPropagation : function ()
            {
                this._event.cancelBubble = true; // for Opera?
                this._event.stopPropagation();
            }
        };
        function T () {}

        return (
        {
            /**
            This internal method is used by jibits.listener.
            */
            _wrapListener : function (handler, id)
            {
                return function (_ev)
                {
                    //zjs.copyProps(ev, methods);
                    T.prototype = _ev;
                    var ev = new T();
                    ev._event = _ev;
                    zjs.copyProps(ev, methods);

                    /*
                    From http://www.quirksmode.org/dom/w3c_events.html:

                    The target property refers to the HTML element the event
                    actually took place on, even when the event handler has
                    been registered not on the HTML element but on one of its
                    ancestors.

                    Mozilla 1.3 and Safari v73 have obligingly mended their
                    earlier target confusion: text nodes are no longer counted
                    as separate targets.

                    Unfortunately Safari v85 has reverted to the incorrect
                    behaviour.
                    */
                    if (ev.target)
                        switch (ev.target.nodeType)
                        {
                         case 3: case 4: // TEXT or CDATA
                            ev.target = ev.target.parentNode; // now ELEMENT
                            break;
                        }

                    return handler(ev);
                }
            }
        });
    }());

//-----------------------------------------------------------------------------

$namespace(jibits, {

/**
Returns the EventManager associated with the given object. The object should be
a window or frame. The EventManager is created on first use and stored in the
meta-data for the object (so the same EventManager will be returned on the next
call).

@param owner The window or frame associated with the EventManager.
@return The EventManager for the given object.
*/
eventMgr : function (owner)
{
    var mgr, o = owner || window, meta = jibits.getMeta(o, true);
    if (!meta || !(mgr = meta.mgr))
    {
        mgr = new jibits.EventManager(o);
        jibits.addMeta(o, { mgr : mgr });
    }
    return mgr;
},

/**
This method "fixes" IE6's lack of proper ":hover" CSS support. The fix is to add
a proper CSS class called "hover" on mouseover and remove it on mouseout. There
are three ways to invoke this:

    1. Directly for a given element:

            jibits.hoverHack(el);

    2. By adding a "hoverhack=1" to the CSS class for the element and invoking
       jibits.parseDocument/Widget et.al.. The "hoverhack=1" component of the
       CSS className is recognized and removed. That is, this:

            [th class="header hoverhack=1"] ... [/th]

       will become this:

            [th class="header"] ... [/th]

    3. By adding a (IE-specific) CSS attribute expression to a CSS rule for the
       elements that should be hacked:

            .lt_IE7 th.header
            {
                hoverHack: expression(jibits.hoverHack(this));
            }

       The "lt_IE7" CSS class is one placed by the ZJS browser decoration method.

In any case, the elements' CSS rule must be expanded to recognize ".hover" instead
of ":hover". It should be noted that the same browser with the hover problem (IE6)
does not properly apply CSS rules with two class qualifiers on an element. For
example:

        th.header.hover {
            background-color: blue;
        }

This rule will not be applied as intended where we want "all TH elements with both
header and hover classes".
*/
hoverHack : function ()
{
    if (!(zjs.browser.ie && zjs.browser.version < 7)) // if (not :hover support)
        return $super.none;

    function hacker (ev)
    {
        var css = [(ev.type == "mouseover" ? "+" : "-") + "hover"];

        jibits.modifyClasses(ev.target, css);
    }

    return function (el)
    {
        el = $elem(el);
        var meta = jibits.getMeta(el);
        if (meta.hoverHacked)
            return;

        var mgr = jibits.eventMgr(), fn = jibits.listener(hacker, el.id);

        mgr.listen(el, "mouseover", fn);
        mgr.listen(el, "mouseout", fn);
        meta.hoverHacked = true;
    }
}(),

/**
Wraps the given method as a normalized event listener. The signature and use of
such event handlers is like so:

        function onFoo (event)
        {
            // event is essentially W3C style.
        }

The event object passed has other properties like pageX and pageY. See this page:

    http://www.w3.org/TR/2000/REC-DOM-Level-2-Events-20001113/events.html#Events-MouseEvent

Also, see these pages:

    http://www.quirksmode.org/dom/w3c_events.html
    http://www.quirksmode.org/dom/w3c_cssom.html

For IE, the srcElement/toElement/fromElement properties are replaced by the
standard target and relatedTarget properties. The raw event is stored as the
_event property.

The "this" pointer is not used, making it good practice to bind a particular
application object:

    var handler = jibits.listener(this.onFoo.bind(this), "foo");

In addition, the W3C methods preventDefault and stopPropagation are provided
for IE. A new method (called "consume") is added that calls both these methods
(and takes other steps on IE) to ensure an event has no other response.

This wrapping is handled automatically by EventManager.

@param handler The handler method to call for events.
@param id The element ID for this handler (needed to resolve IE issues).
@return The handler method to actually register for events.
*/
listener : function (handler, id)
{
    var binding = "listener", fm;

    for (fm = zjs.getMeta(handler, true);
                    fm && fm.target;
                    fm = zjs.getMeta(fm.target, true))
        if (fm.binding === binding)
            return handler;

    var ret = jibits._wrapListener(handler, id);
    return zjs.addMeta(ret, { binding : binding, target : handler, tid : id });
}

}); // namespace jibits
